##    Copyright 2007 David Blewett
##
##   Licensed under the Apache License, Version 2.0 (the "License");
##   you may not use this file except in compliance with the License.
##   You may obtain a copy of the License at
##
##       http://www.apache.org/licenses/LICENSE-2.0
##
##   Unless required by applicable law or agreed to in writing, software
##   distributed under the License is distributed on an "AS IS" BASIS,
##   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
##   See the License for the specific language governing permissions and
##   limitations under the License.
## Author: David Blewett
## Contact: david@dawninglight.net
from message import Address, Envelope, ENV_FIELDS

"""Miscellaneous utilities."""

__all__ = [
    'parse_fetch_response', 
    'internaldate2datetime',
    'wildcard2regexp',
    'split_quoted',
    'split_on',
    'collapse_strings',
    'parse_nested_parens',
    ]

from datetime import tzinfo
import re, string, time, types

Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
        'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
#InternalDate = re.compile(r'"'
#    r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9]) '
#    r'(?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9]) '
#    r'(?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
#    r'"')
InternalDate = re.compile(r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z]{2})-(?P<year>[0-9]{4}) (?P<hour>[0-9]{2}):(?P<min>[0-9]{2}):(?P<sec>[0-9]{2}) (?P<zonen>[-+])(?P<zoneh>[0-9]{2})(?P<zonem>[0-9]{2})')

IMAP_FLAGS = {
    'Seen': False, 'Answered': False, 'Flagged': False, 
    'Deleted': False, 'Draft': False,  'Recent': False
}


class IDate_tzinfo(tzinfo):
    def utcoffset(self, dt):
        ''
        
    def dst(self, dt):
        ''
        
    def tzname(self, dt):
        ''
        
    def fromutc(self, dt):
        ''

    
def parse_fetch_response(self, fetch, cmd):
    """Parses IMAP fetch response string.

    @type C{FetchCommand}
    @param cmd: command passed to IMAP server.

    @type C{str}
    @param fetch: raw IMAP response string.

    @rtype: C{dict}
    @return: dict of IMAP items.
    envelope=date, subject, msg_from, sender, reply_to, to, cc, bcc, in_reply_to, msg_id
    address=personal_name, source_route, mailbox_name, host_name
    """
    if cmd.section:
        fetch = fetch[:fetch.rfind('{')-1] + ')' + fetch[fetch.rfind('{')-1:]
    s, parse_fetch = parse_nested_parens(fetch)
    response = dict(zip(parse_fetch[::2], parse_fetch[1::2]))
    response['SEQID'] = int(s)
    
    if cmd.section:
        response['RFC822.SIZE'] = parse_fetch[-1]

    for k, to_python in FETCH_RESPONSES.items():
        if k in response:
            response[k] = to_python(response[k])
    
    return response

def env2python(env):
    e = Envelope()                
    for i in range(len(ENV_FIELDS)):
        f = ENV_FIELDS[i]
        if isinstance(env[i], list):
            #list of address elements
            for a in env[i]:
                addr = Address(a)
                env.__dict__[f].append(addr)
        else:
            env.__dict__[f] = env[i]                    
    return e

def flags2bool(flags):
    cflags = IMAP_FLAGS.copy()
    for f in flags:
        f.lstrip('\\')
        cflags[f.title()] = True
    return cflags

def internaldate2datetime(idate):
    """Convert IMAP4 INTERNALDATE to UT.

    @type {str}: A MessageSet object with the list of messages requested
    @param idate: String representation of IMAP4 internal date.

    @rtype: C{datetime}
    @return: A datetime.datetime object representing the internal date.
    """
    mo = InternalDate.match(idate)
    if not mo:
        return None

    mon = Mon2num[mo.group('mon')]
    zonen = mo.group('zonen')
    day = int(mo.group('day'))
    year = int(mo.group('year'))
    hour = int(mo.group('hour'))
    min = int(mo.group('min'))
    sec = int(mo.group('sec'))
    zoneh = int(mo.group('zoneh'))
    zonem = int(mo.group('zonem'))

    # INTERNALDATE timezone must be subtracted to get UT

    zone = (zoneh*60 + zonem)*60
    if zonen == '-':
        zone = -zone

    tt = (year, mon, day, hour, min, sec, -1, -1, -1)

    utc = time.mktime(tt)

    # Following is necessary because the time module has no 'mkgmtime'.
    # 'mktime' assumes arg in local timezone, so adds timezone/altzone.

    lt = time.localtime(utc)
    if time.daylight and lt[-1]:
        zone = zone + time.altzone
    else:
        zone = zone + time.timezone

    return time.localtime(utc - zone)

def wildcard2regexp(wildcard, delim=None):
    wildcard = wildcard.replace('*', '(?:.*?)')
    if delim is None:
        wildcard = wildcard.replace('%', '(?:.*?)')
    else:
        wildcard = wildcard.replace('%', '(?:(?:[^%s])*?)' % re.escape(delim))
    return re.compile(wildcard, re.I)

#Following code courtesy of Twisted: Mail
#http://twistedmatrix.com/trac/browser/trunk/twisted/mail/imap4.py
def split_quoted(s):
    """Split a string into whitespace delimited tokens

    Tokens that would otherwise be separated but are surrounded by \"
    remain as a single token.  Any token that is not quoted and is
    equal to \"NIL\" is tokenized as C{None}.

    @type s: C{str}
    @param s: The string to be split

    @rtype: C{list} of C{str}
    @return: A list of the resulting tokens

    @raise MismatchedQuoting: Raised if an odd number of quotes are present
    """
    s = s.strip()
    result = []
    inQuote = inWord = start = 0
    for (i, c) in zip(range(len(s)), s):
        if c == '"' and not inQuote:
            inQuote = 1
            start = i + 1
        elif c == '"' and inQuote:
            inQuote = 0
            result.append(s[start:i])
            start = i + 1
        elif not inWord and not inQuote and c not in ('"' + string.whitespace):
            inWord = 1
            start = i
        elif inWord and not inQuote and c in string.whitespace:
            if s[start:i] == 'NIL':
                result.append(None)
            else:
                result.append(s[start:i])
            start = i
            inWord = 0
    if inQuote:
        raise MismatchedQuoting(s)
    if inWord:
        if s[start:] == 'NIL':
            result.append(None)
        else:
            result.append(s[start:])
    return result


def split_on(sequence, predicate, transformers):
    result = []
    mode = predicate(sequence[0])
    tmp = [sequence[0]]
    for e in sequence[1:]:
        p = predicate(e)
        if p != mode:
            result.extend(transformers[mode](tmp))
            tmp = [e]
            mode = p
        else:
            tmp.append(e)
    result.extend(transformers[mode](tmp))
    return result

def collapse_strings(results):
    """
    Turns a list of length-one strings and lists into a list of longer
    strings and lists.  For example,

    ['a', 'b', ['c', 'd']] is returned as ['ab', ['cd']]

    @type results: C{list} of C{str} and C{list}
    @param results: The list to be collapsed

    @rtype: C{list} of C{str} and C{list}
    @return: A new list which is the collapsed form of C{results}
    """
    copy = []
    begun = None
    listsList = [isinstance(s, types.ListType) for s in results]

    pred = lambda e: isinstance(e, types.TupleType)
    tran = {
        0: lambda e: split_quoted(''.join(e)),
        1: lambda e: [''.join([i[0] for i in e])]
    }
    for (i, c, isList) in zip(range(len(results)), results, listsList):
        if isList:
            if begun is not None:
                copy.extend(split_on(results[begun:i], pred, tran))
                begun = None
            copy.append(collapse_strings(c))
        elif begun is None:
            begun = i
    if begun is not None:
        copy.extend(split_on(results[begun:], pred, tran))
    return copy


def parse_nested_parens(s, handleLiteral = 1):
    """Parse an s-exp-like string into a more useful data structure.

    @type s: C{str}
    @param s: The s-exp-like string to parse

    @rtype: C{list} of C{str} and C{list}
    @return: A list containing the tokens present in the input.

    @raise MismatchedNesting: Raised if the number or placement
    of opening or closing parenthesis is invalid.
    """
    s = s.strip()
    inQuote = 0
    contentStack = [[]]
    try:
        i = 0
        L = len(s)
        while i < L:
            c = s[i]
            if inQuote:
                if c == '\\':
                    contentStack[-1].append(s[i+1])
                    i += 2
                    continue
                elif c == '"':
                    inQuote = not inQuote
                contentStack[-1].append(c)
                i += 1
            else:
                if c == '"':
                    contentStack[-1].append(c)
                    inQuote = not inQuote
                    i += 1
                elif handleLiteral and c == '{':
                    end = s.find('}', i)
                    if end == -1:
                        raise ValueError, "Malformed literal"
                    literalSize = int(s[i+1:end])
                    contentStack[-1].append((s[i+1:end],))
                    i = end + 1
                elif c == '(' or c == '[':
                    contentStack.append([])
                    i += 1
                elif c == ')' or c == ']':
                    contentStack[-2].append(contentStack.pop())
                    i += 1
                else:
                    contentStack[-1].append(c)
                    i += 1
    except IndexError:
        raise MismatchedNesting(s)
    if len(contentStack) != 1:
        raise MismatchedNesting(s)
    return collapse_strings(contentStack[0])

FETCH_RESPONSES = {
    'SEQID': int,
    'INTERNALDATE': internaldate2datetime,
    'FLAGS': flags2bool, 
    'ENVELOPE': env2python, 
    'EXISTS': int,
    'RECENT': int,
    'UNSEEN': int,
    'PERMANENTFLAGS': parse_nested_parens,
    'UIDNEXT': int,
    'UIDVALIDITY': int,
}
